package thomas.studio.spring.security.ldap.securityprovider;

import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

import javax.naming.Name;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.AuthenticatedLdapEntryContextCallback;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.LdapEntryIdentification;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.ldap.userdetails.LdapUserDetails;
import org.springframework.stereotype.Component;

import thomas.studio.spring.security.ldap.service.User;

@Component
public class LdapAuthenticationProvider implements AuthenticationProvider {

	@Autowired
	private LdapTemplate ldapTemplate;

	private String searchFilter = "(&(objectclass=inetOrgPerson)(uid=%1$s))";

	private String searchBase = "ou=user";

	private static final Log LOG = LogFactory.getLog(LdapAuthenticationProvider.class);

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Authentication result = null;
		if(authentication instanceof UsernamePasswordAuthenticationToken){
			UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken)authentication;
			String username = userToken.getName();
			String password = (String) userToken.getCredentials();
			UserDetails user = this.authenticate(username, password);
			//User user = ldapService.getUserById(username);
			//this.ldapTemplate.authenticate(base, filter, password, callback);
			result = new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
		}
		return result;
	}

	private UserDetails authenticate(String username, String password) {
		Callback callback = new Callback();
	 	boolean authenticated = this.ldapTemplate.authenticate(
				this.searchBase, String.format(this.searchFilter, username), password, callback);
		if(!authenticated){
			return null;
		}
		return callback.getUserDetails();
	}

	@SuppressWarnings("rawtypes")
	class Callback implements AuthenticatedLdapEntryContextCallback, ContextMapper{
		private LdapUserDetails userDetails  = null;

		@Override
		public void executeWithContext(DirContext ctx, LdapEntryIdentification ldapEntryIdentification) {
			try {
				Object context = ctx.lookupLink(ldapEntryIdentification.getRelativeName());
				userDetails = (LdapUserDetails)this.mapFromContext(context);
			} catch (NamingException e) {
				throw new RuntimeException("Failure to look up ldap entry! " + ldapEntryIdentification, e);
			}

		}
		/**
		 * @param context
		 * @return
		 * @throws NamingException
		 */
		@Override
		public Object mapFromContext(Object ctx) throws NamingException {
			DirContextOperations context = (DirContextOperations)ctx;
			User.Essence user = new User.Essence(context);
			/* resolve java.lang.IllegalArgumentException: username must not be null in org.springframework.ldap.core.LdapTemplate*/
			user.setUsername(context.getStringAttribute("uid"));
			Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
			for(NamingEnumeration eumeration = context.getAttributes().getAll(); eumeration.hasMore();){
				Attribute attribute = (Attribute) eumeration.next();
				String attributeId = attribute.getID();
				String attributeValue = String.valueOf(attribute.get());
				LOG.info(attributeId + "=" + attributeValue);
			}

			Name dn = context.getDn();
			/* if  dn: cn=Thomas Chan,cn=Stock-Users,ou=people,dc=thomas-studio,dc=com
			 * then it will be granted the roles: ROLE_USER, ROLE_STOCK-USERS, STOCK_PEOPLE */
			String cn = context.getStringAttribute("cn");
			for(Enumeration<String> eumeration = dn.getAll(); eumeration.hasMoreElements();){
				String dnSubElement = eumeration.nextElement();
				LOG.info("dnSubElement: " + dnSubElement);
				String dnSubElementId = dnSubElement.split("=")[0];
				String dnSubElementValue = dnSubElement.split("=")[1];
				if("cn".equals(dnSubElementId) || "ou".equals(dnSubElementId)){
					if(cn.equals(dnSubElementValue)){
						authorities.add(new SimpleGrantedAuthority("ROLE_" + "USER"));
					}else{
						authorities.add(new SimpleGrantedAuthority("ROLE_" + dnSubElementValue.toUpperCase()));
					}
				}
			}
			user.setAuthorities(authorities);

			LdapUserDetails userDetails  =  user.createUserDetails();
			return userDetails;
		}

		public LdapUserDetails getUserDetails() {
			return userDetails;
		}




	}

	@Override
	public boolean supports(Class<?> arg0) {
		return true;
	}

}
